/* @flow */

import config from "core/config";
import { addHandler, addProp, getBindingAttr } from "compiler/helpers";
import { genComponentModel, genAssignmentCode } from "compiler/directives/model";

let warn;

// in some cases, the event used has to be determined at runtime
// so we used some reserved tokens during compile.
export const RANGE_TOKEN = "__r";
export const CHECKBOX_RADIO_TOKEN = "__c";

export default function model(el: ASTElement, dir: ASTDirective, _warn: Function): ?boolean {
  warn = _warn;
  const value = dir.value;
  const modifiers = dir.modifiers;
  const tag = el.tag;
  const type = el.attrsMap.type;

  if (process.env.NODE_ENV !== "production") {
    // inputs with type="file" are read only and setting the input's
    // value will throw an error.
    if (tag === "input" && type === "file") {
      warn(
        `<${el.tag} v-model="${value}" type="file">:\n` + `File inputs are read only. Use a v-on:change listener instead.`,
        el.rawAttrsMap["v-model"]
      );
    }
  }

  if (el.component) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false;
  } else if (tag === "select") {
    genSelect(el, value, modifiers);
  } else if (tag === "input" && type === "checkbox") {
    genCheckboxModel(el, value, modifiers);
  } else if (tag === "input" && type === "radio") {
    genRadioModel(el, value, modifiers);
  } else if (tag === "input" || tag === "textarea") {
    genDefaultModel(el, value, modifiers);
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false;
  } else if (process.env.NODE_ENV !== "production") {
    warn(
      `<${el.tag} v-model="${value}">: ` +
        `v-model is not supported on this element type. ` +
        "If you are working with contenteditable, it's recommended to " +
        "wrap a library dedicated for that purpose inside a custom component.",
      el.rawAttrsMap["v-model"]
    );
  }

  // ensure runtime directive metadata
  return true;
}

function genCheckboxModel(el: ASTElement, value: string, modifiers: ?ASTModifiers) {
  const number = modifiers && modifiers.number;
  const valueBinding = getBindingAttr(el, "value") || "null";
  const trueValueBinding = getBindingAttr(el, "true-value") || "true";
  const falseValueBinding = getBindingAttr(el, "false-value") || "false";
  addProp(
    el,
    "checked",
    `Array.isArray(${value})` +
      `?_i(${value},${valueBinding})>-1` +
      (trueValueBinding === "true" ? `:(${value})` : `:_q(${value},${trueValueBinding})`)
  );
  addHandler(
    el,
    "change",
    `var $$a=${value},` +
      "$$el=$event.target," +
      `$$c=$$el.checked?(${trueValueBinding}):(${falseValueBinding});` +
      "if(Array.isArray($$a)){" +
      `var $$v=${number ? "_n(" + valueBinding + ")" : valueBinding},` +
      "$$i=_i($$a,$$v);" +
      `if($$el.checked){$$i<0&&(${genAssignmentCode(value, "$$a.concat([$$v])")})}` +
      `else{$$i>-1&&(${genAssignmentCode(value, "$$a.slice(0,$$i).concat($$a.slice($$i+1))")})}` +
      `}else{${genAssignmentCode(value, "$$c")}}`,
    null,
    true
  );
}

function genRadioModel(el: ASTElement, value: string, modifiers: ?ASTModifiers) {
  const number = modifiers && modifiers.number;
  let valueBinding = getBindingAttr(el, "value") || "null";
  valueBinding = number ? `_n(${valueBinding})` : valueBinding;
  addProp(el, "checked", `_q(${value},${valueBinding})`);
  addHandler(el, "change", genAssignmentCode(value, valueBinding), null, true);
}

function genSelect(el: ASTElement, value: string, modifiers: ?ASTModifiers) {
  const number = modifiers && modifiers.number;
  const selectedVal =
    `Array.prototype.filter` +
    `.call($event.target.options,function(o){return o.selected})` +
    `.map(function(o){var val = "_value" in o ? o._value : o.value;` +
    `return ${number ? "_n(val)" : "val"}})`;

  const assignment = "$event.target.multiple ? $$selectedVal : $$selectedVal[0]";
  let code = `var $$selectedVal = ${selectedVal};`;
  code = `${code} ${genAssignmentCode(value, assignment)}`;
  addHandler(el, "change", code, null, true);
}

function genDefaultModel(el: ASTElement, value: string, modifiers: ?ASTModifiers): ?boolean {
  const type = el.attrsMap.type;

  // warn if v-bind:value conflicts with v-model
  // except for inputs with v-bind:type
  if (process.env.NODE_ENV !== "production") {
    const value = el.attrsMap["v-bind:value"] || el.attrsMap[":value"];
    const typeBinding = el.attrsMap["v-bind:type"] || el.attrsMap[":type"];
    if (value && !typeBinding) {
      const binding = el.attrsMap["v-bind:value"] ? "v-bind:value" : ":value";
      warn(
        `${binding}="${value}" conflicts with v-model on the same element ` + "because the latter already expands to a value binding internally",
        el.rawAttrsMap[binding]
      );
    }
  }

  const { lazy, number, trim } = modifiers || {};
  const needCompositionGuard = !lazy && type !== "range";
  const event = lazy ? "change" : type === "range" ? RANGE_TOKEN : "input";

  let valueExpression = "$event.target.value";
  if (trim) {
    valueExpression = `$event.target.value.trim()`;
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`;
  }

  let code = genAssignmentCode(value, valueExpression);
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`;
  }

  addProp(el, "value", `(${value})`);
  addHandler(el, event, code, null, true);
  if (trim || number) {
    addHandler(el, "blur", "$forceUpdate()");
  }
}
